Skip to content

Conversation

@yunfachi
Copy link
Owner

@yunfachi yunfachi commented Aug 29, 2025

tasks

  • README.md
  • Documentation
  • delib.module, delib.host, delib.rice wrappers

problems

  • [ ]

lib/default.nix Outdated

inherit (_callLib ./maintainers.nix) maintainers;
attrset = delib._callLib ./attrset.nix;
inherit (delib.attrset) getAttrByStrPath setAttrByStrPath hasAttrs;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe these functions should be kept under a separate "namespace"? like delib.attrs.getStr, delib.attrs.setStr and delib.attrs.has.

with this approach functions which should be first priority to the user (options) are in the global namespace, and other optional, but useful, are located under some kind of a namespace.

and repeated attr with the long function name just bothers me, to be honest. if the module is already named attrset, it would be pretty strange if functions don't work on attributes, but on something else

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, an interesting pattern, but the functions will still be called delib.attrs.setByStrPath, delib.attrs.getByStrPath, and delib.attrs.hasKeys.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this possible to compile all available modules (or for selected module systems) at once? otherwise, as can be seen in the example, it needs to be called for each moduleSystem, and not that performance is very critical, but going through all the files recursively several times for the same task feels wrong. and that's considering that the approach of denix is to include all systems in one file - why not compile them at once? the only issue is how to implement it, though...

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I think compileModules would be a suitable name.

@shimeoki
Copy link
Contributor

one more thing. while i was testing #59, i noticed that denix defines all options system-wide.

example:

i have a nixos and a home-manager vanilla modules. to make home-manager module independent from the user, i don't specify the full path to the option like home-manager.users.d.programs.git, but just programs.git.

to make this work, it's required to import the module with all home-manager modules like home-manager.users.d = ./hm.nix.

this approach allows for a multi-user configuration. if you define custom options in these modules, they always have a separate scope: home-manager.users.${user}, and each user can set these options to whichever values they like.

i found about this trait of the library while i was migrating my git and gpg home-manager modules: while defined as delib modules, my other home-manager vanilla modules were unable to find my options to check for the enabled git, for example.

this happened because my modules were dependent on themselves and not the options of home-manager. for example, to see is git enabled or not, i checked shimeoki.git.enable option and not programs.git.enable.

and because these shimeoki options were defined at the home-manager level, their full path looks something like home-manager.users.d.shimeoki.git.enable and so on. denix, on the other hand, always defines these options like ${myconfigName}.git.enable (full path), if module system is not home.

as multi-user configurations were already considered before (#37), i think a rewrite is a good time to think about:

  • should this be implemented or not
  • if so, then how
  • if possible, then how to make it backwards compatible

let's be real: almost everyone uses a single user on their desktops. root user doesn't count. and for a "personal working machine" environment i really like that denix defines options for all "platforms" in one place. for your desktop, you probably don't care about security and option scopes, but some modules are pretty inconvenient to enable on different "platforms": stylix is system-wide for the enable, but targets are located in home-manager; niri from the niri-flake is enabled on nixos level, but all options are user-wide. it feels good to define them from a single file and not import from different levels in the hierarchy, because these options should work together.

it applies to the config part of the problem: assigning options works perfectly. but why the same options in the same file (delib.module) behave differently depending on the platform? let's assume that myconfigName is my username. if platform is nixos or darwin, then options have shimeoki prefix. if platform is home, then options suddenly have home-manager.users.${user}.shimeoki prefix.

right now it's not a problem, because myconfig parameter is always translated to the correct attribute set in either case. but if modules can be exported (as proposed in the rewrite), then it becomes a problem. denix translates everything to plain nix modules, so it shouldn't be required that the target user of the module should use denix as well - it's for the author's convenience only.

so, this user imports the nixos module. and then, to configure options from the module, the user is just obliged to use the "nixos scope" (global scope) - the user cannot configure anything from home-manager modules, because the options are located at the top level.

in a way, that makes sense: you imported a nixos module - use it from the nixos scope. but still, why cannot you change the parameters that only change the home-manager config from the home-manager scope?

the thing is i don't actually know is this good or not. i already shot myself in the foot a couple of times while trying to divide my configuration into fully separate home-manager and nixos modules, because some things should be done for the user on the system level: stylix wallpaper, xkb layout, autologin and more.

maybe something like homeOptions is possible, but to make it work, then default options should be nullified like nixos and darwin configs if the module system is home, which is not backwards compatible. also, i feel like i am just now trying to push my own vision onto your project, while it should define your own ideas. right now i thought it's just an important concern to consider in the planning.

@yunfachi
Copy link
Owner Author

should this be implemented or not

Should be.

Actually, I didn't completely understand the text, but here's what I can say since it's relevant:

The old solution creates all options in the user's root module system only under myconfig.
If you use Home Manager as a NixOS module in a NixOS system, Denix sets the Home Manager configuration via config.home-manager.users.<user>, and the myconfig option is not created there.
The implication: Home Manager modules in NixOS are not separate modules imported by the user's Home Manager - they are configurations defined in the NixOS system.

In the new solution, the "myconfig" system is currently implemented (I haven't pushed everything due to drawbacks) not in system configurations (NixOS, Home Manager, etc.), but in the Denix module system. This has the following pros and cons:

Cons:

  1. The enable option of each module cannot be set based on NixOS, Home Manager, etc. system configurations. I think this is quite bad. But if this is sacrificed, applying ifEnabled and ifDisabled configurations would again require lib.mkIf, and conditional imports would again be impossible. This isn't too bad, but there's a chance it won't reduce "random" infinite recursion errors compared to master.

Pros:

  1. Now {moduleSystem}.[always,ifEnabled,ifDisabled] is not part of config; it's a full module added directly to the module system's modules without modifications. This allows, for example, conditional imports.

Facts:

  1. denix.modules.<name>.options are still provided to the module system. This is more of a drawback than a benefit, but otherwise they wouldn't be able to access module system configurations, even pkgs.

At the moment, this is the main problem in the rewrite, and I currently have no solution for it.

@shimeoki
Copy link
Contributor

oh. that's pretty hard to grasp. can i verify some things?

if i understood it correctly, then the new denix module system will control itself from the denix module system itself?

so, for example, if you have a gpg configuration for git which you only want to import if gpg module is enabled, you are required to check denix's gpg module's enable option and not the system option for programs.gpg.enable? personally, i already did my configuration like this, so it's not an issue for me. i don't think that's necessarily bad, because it makes the modules self-contained.

if the modules are planned to be exportable, then how the options for them would be presented? for example, right now i have the following in the home-manager module of my user:

{ inputs, ... }:
{
    imports = [
        inputs.self.homeModules.shimeoki
		# ...
    ];

    shimeoki = {
        enable = true;

        nh.flake = "/home/d/nixconfig";

		waybar = {
			keyboard-state.enable = false;
	        cava.enable = false;
	        mpd.enable = false;
		};

		# ...
    };

	# ...
}

the user imports my module and configures it. very simple. how "denix module options" would be translated to this format, if they are dependent on themselves? via providing delib.denixConfiguration { options = { ... }; }, compiling the module and importing it without providing the options afterwards?

then, if i import a compiled nixos module from denix, does it import a home-manager module as well? if so, then how options for the home-manager denix configuration can be changed at the user level?

@yunfachi
Copy link
Owner Author

yunfachi commented Aug 30, 2025

if i understood it correctly, then the new denix module system will control itself from the denix module system itself?

yes

if the modules are planned to be exportable, then how the options for them would be presented?

by custom function

/*
applyMyConfig =
{ myconfig, ... }:
{
imports = myconfig.type.getSubModules;
};
*/

then, if i import a compiled nixos module from denix, does it import a home-manager module as well?

delib.compileModule can create a module that you wrote, it does not add any pre-built home-manager modules to your configuration.


The only two solutions that I was able to come up with:

  1. All options (including enable) are in Denix. They cannot take values from your configuration, not even use pkgs. Theoretically, it might still be possible to use values from configurations, but these values cannot in any way affect enable.
  2. All options (including enable) are in your configuration, and only there. In that case, all ifEnabled and ifDisabled cannot contain imports, since their activation happens in the configuration itself using lib.mkIf rather than in Denix. In this solution, there is a potentially higher chance of accidentally causing infinite recursion.

@shimeoki
Copy link
Contributor

delib.compileModule can create a module that you wrote, it does not add any pre-built home-manager modules to your configuration.

oh. my wording is pretty bad. i meant the following:

if i compile a nixos denix module, does it contain a home module as well? so, are platforms (nixos, darwin, home) fully separated or not?

i actually just cannot get this from the code. i am not at that level of functional programming wizardry right now.

The only two solutions that I was able to come up with:

  1. All options (including enable) are in Denix. They cannot take values from your configuration, not even use pkgs. Theoretically, it might still be possible to use values from configurations, but these values cannot in any way affect enable.
  2. All options (including enable) are in your configuration, and only there. In that case, all ifEnabled and ifDisabled cannot contain imports, since their activation happens in the configuration itself using lib.mkIf rather than in Denix. In this solution, there is a potentially higher chance of accidentally causing infinite recursion.

so, it's like 1 is the new approach for now, and 2 is the current (master branch) approach?

if i got your point, the main concern is conditional imports.

i think, there are three main use cases to use conditional imports:

  1. to reduce the parse/evaluation time
  2. to not pollute the "namespace" with the additional options
  3. to not get the "side-effects", if the module does something automatically without the user's knowledge

personally, i think points 3 and 2 are not a problem:

  1. if a module has unintended side-effects, that's just a bad design. because of this i proposed "cascade enabling" into the current denix in the first place: importing everything automatically seems convenient in a personal configuration, but that behaviour should be enabled explicitly.

  2. namespace pollution is not a big deal. if they are defined in their own attribute sets and don't "inject" into the other module's options - that's ok. for example, what is the chance that your configuration relies on the shimeoki (insert any username) top-level attribute? and that's already what denix and many users with vanilla modules do.

i even think that usually you want to have the opposite of the second point - just read all the available options. otherwise, if you want to have a "safe" module system, then you need to check for every option availability and give a default value if the check failed.

but the first point... i just recently started using nixos, and my build times are already a minute in average if nothing is being "really built" - only the parse and evaluation time. "you want to change a single option right now? - please, wait a minute.". it's terrifying for me to imagine build times for bigger systems. i think that's the main benefit.

but for the performance, i think not compiling the unneeded modules for whole platforms or unused hosts/users is good enough. and there should be no problems with conditional imports here, because these options can be defined at the denix level before the compile process.

if an another point is "avoiding accidental infinite recursions", then i don't think that should be considered. if the user made a mistake in defining the module, it's their fault. if that's possible, denix should just check ifEnabled and ifDisabled blocks for imports attributes and give an error if at least one was found.

so i am voting for the second approach. with this, denix is closer to the vanilla modules and the current implementation, while giving a bit more freedom to the user.

@yunfachi
Copy link
Owner Author

if i compile a nixos denix module, does it contain a home module as well? so, are platforms (nixos, darwin, home) fully separated or not?

it will not contain a home manager module. fully separated.

so, it's like 1 is the new approach for now, and 2 is the current (master branch) approach?

yes, but not completely. I don't push all the changes.

if i got your point, the main concern is conditional imports.

no, this is just an example of what could become possible, maybe not the best one. the main concern in the first solution is a lower chance of "accidental" infinite recursion errors, which occur when you do something extraordinary, though it's not guaranteed that this will happen. This is more of a master branch problem that shouldn't be repeated.

but the first point... i just recently started using nixos, and my build times are already a minute in average if nothing is being "really built" - only the parse and evaluation time. "you want to change a single option right now? - please, wait a minute.". it's terrifying for me to imagine build times for bigger systems. i think that's the main benefit.

these two solutions are almost the same in terms of performance. your nixos-rebuild switch is slow because of home manager.

blocks for imports attributes and give an error if at least one was found.

unfortunately, it doesn't work that way. well, in the second solution, the user provides not the module itself, but the config value. however, I can use the unifyModuleSyntax function from nixpkgs, which allows convenient support for adding both options and config, but not imports.


I think I can give brief descriptions for each solution:

  1. Isolated but predictable; also allows setting options that do not affect enable within the module systems themselves. However, it can be misleading and, in some cases, lead to writing boilerplate code outside the bounds of clean code and pattern.
  2. More convenient, but less predictable due to the higher chance of mistakes being made easily.

Well, I think the second solution would be preferable.

@shimeoki
Copy link
Contributor

if i compile a nixos denix module, does it contain a home module as well? so, are platforms (nixos, darwin, home) fully separated or not?

it will not contain a home manager module. fully separated.

then, if the second approach is chosen, aren't multi-user configurations supported OOTB?

  1. user compiles the home denix module in the flake
  2. this module only contains home-manager config definitions at the top-level, so it could be safely imported manually by the user or automatically in shared modules
  3. options to configure the module are available at the home-manager.users.${user} level, so could be set by the user

these two solutions are almost the same in terms of performance. your nixos-rebuild switch is slow because of home manager.

oh. thanks for the insight. i guess i underestimated nix.

blocks for imports attributes and give an error if at least one was found.

unfortunately, it doesn't work that way. well, in the second solution, the user provides not the module itself, but the config value. however, I can use the unifyModuleSyntax function from nixpkgs, which allows convenient support for adding both options and config, but not imports.

well, pretty sad.


also, i have one more point towards the second solution. option search engines like nuschtos search.

a search should be pretty useful in complex configurations. maybe not so useful in a personal configuration, but if the modules are planned to be exportable (so "sharing modules" concept exists in the library), i think the support for this kind of tooling is great.

though i haven't succeeded in deploying this tool in my configuration, it should just take a module and display all options automatically. there are probably more instruments like this, and i think they expect "vanilla" options to be available. so, the second solution should work OOTB once again.

but if the first solution is chosen and support for these kind of search engines is desired, then either:

  • support for the specific tool should be provided (like exporting denix options to a json file)
  • denix should support the export of isolated options in the denix module system to vanilla options

i don't think this is worth it. and there are probably more tools which expect plain vanilla options to be available.

@yunfachi
Copy link
Owner Author

then, if the second approach is chosen, aren't multi-user configurations supported OOTB?

Yes, but making users manually call denix.lib.denixConfiguration in their configuration and then run compileModule isn't the right approach. A function similar to the old denix.lib.configurations should be added to simplify this. The new function should stay clean, so that most or all of its arguments can be defined directly in the Denix module system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants